Service와 Transaction

✒️ 2025-06-30 11:46 내용 수정

스프링부트3 자바 백엔드 개발입문 내용 참고 및 정리


Service

서버의 핵심 기능인 비즈니스 로직을 처리하는 순서를 총괄하는 계층


REST API에 Service 계층 추가

  1. 먼저 com.example.package_name 패키지에 service 패키지를 추가하고 ArticleService 클래스를 생성한다.
    • @Service Annotation은 해당 클래스를 Service로 인식해 Service 객체를 생성한다.
    • Service 클래스 내에 ArticleRepository를 자동 주입하여 Repository를 통해 DB에 접근할 수 있도록 설정한다.

jpa_service 1.png

  1. ArticleApiController에서 작성한 내용들 중 DB에 접속하는 Repository 동작 부분을 ArticleService 클래스의 메소드로 옮기고, ArticleApiController의 메소드에는 ArticleService의 메소드 호출로 수정한다.
    • 서버의 상태 메시지를 보내는 ResponseEntity 객체는 ArticleApiController에서 생성해서 반환하고, ArticleService에선 ArticleApiController에서 사용할 수 있는 반환값들을 가진 메소드로 설정한다.
package com.example.demo.api;  
  
import com.example.demo.DTO.ArticleForm;  
import com.example.demo.entity.Article;  
import com.example.demo.service.ArticleService;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.http.HttpStatus;  
import org.springframework.http.ResponseEntity;  
import org.springframework.web.bind.annotation.*;  
  
import java.util.ArrayList;  
  
@RestController // REST를 적용한 Controller 사용 표시  
@RequestMapping("/api")  
@Slf4j // simple logging facade for java  
public class ArticleApiController {  
  
    @Autowired  
    private ArticleService articleService;  
  
    // GET  
    @GetMapping("articles") // 게시글 전체 조회  
    public ArrayList<Article> index() {  
        return articleService.index(); // Service에서 DB 동작 수행  
    }  
  
    @GetMapping("articles/{id}") // 특정 게시글 조회  
    public Article show(@PathVariable Long id) {  
        return articleService.show(id);  
    }  
  
    // POST  
    @PostMapping("articles") // 새 글 작성  
    public ResponseEntity<Article> create(@RequestBody ArticleForm dto) { 
    // 요청 시 본문에 포함되는 데이터를 사용  
        Article created = articleService.create(dto);  
        // 추가하려는 데이터의 상태에 따라 서버 상태 코드를 다르게 반환  
        // null : 새 글을 추가해야 하는데 이미 id가 존재할 때(현재 자동 id 생성 적용된 상태)  
        return (created != null) ?  
                ResponseEntity.status(HttpStatus.OK).body(created) :  
                ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);  
    }  
  
    // PATCH  
    @PatchMapping("articles/{id}") // 기존 글 수정  
    public ResponseEntity<Article> update( // 반환형에 유의한다.  
		@PathVariable Long id,  
		@RequestBody ArticleForm dto  
    ) { // 요청 시 본문에 포함되는 데이터를 사용  
        Article updated = articleService.update(id, dto);  
        return (updated != null) ?  
                ResponseEntity.status(HttpStatus.OK).body(updated) :  
                ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);  
    }  
  
    // DELETE  
    @DeleteMapping("articles/{id}")  
    public ResponseEntity<Article> delete(@PathVariable Long id) { // 글 삭제  
        Article deleted = articleService.delete(id);  
        return (deleted == null) ?  
                ResponseEntity.status(HttpStatus.OK).build() :  
                ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);  
    }  
}
package com.example.demo.service;  
  
import com.example.demo.DTO.ArticleForm;  
import com.example.demo.entity.Article;  
import com.example.demo.repository.ArticleRepository;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.http.HttpStatus;  
import org.springframework.http.ResponseEntity;  
import org.springframework.stereotype.Service;  
import org.springframework.web.bind.annotation.*;  
  
import java.util.ArrayList;  
  
@Service  
public class ArticleService {  
    @Autowired  
    private ArticleRepository articleRepository;  
  
    public ArrayList<Article> index() { // 게시글 전체 조회  
        return (ArrayList<Article>) articleRepository.findAll();  
    }  
  
    public Article show(@PathVariable Long id) { // 특정 게시글 조회  
        return articleRepository.findById(id).orElse(null);  
    }  
  
    public Article create(ArticleForm dto) { // 새 글 작성  
        Article article = dto.toEntity();  
  
        // 새 글을 생성해야 하는데 이미 같은 id 데이터가 존재하면 안됨  
        // 이미 존재하는 id 데이터에 POST 요청을 넣으면 데이터를 수정하기에 REST API에 맞지 않음  
        if (article.getId() != null) {  
            return null;  
        }  
        return articleRepository.save(article);  
    }  
  
    public Article update(Long id, ArticleForm dto) { // 기존 글 수정  
        // 들어온 데이터 변환  
        Article article = dto.toEntity();  
  
        // 수정 대상 조회  
        Article target = articleRepository.findById(id).orElse(null);  
  
        // 잘못된 요청 처리  
        // 해당 id의 데이터가 없거나, 요청 id와 수정할 데이터의 id가 다른 경우  
        if (target == null || id != article.getId()) {  
            // 400 코드 전송  
            return null;  
        }  
  
        // DB에 수정 내용 저장  
        target.patch(article); // 기존 데이터에 새 데이터 붙이기(일부만 수정 시 null 방지)  
        Article updated = articleRepository.save(article);  
        // 200 코드와 수정 결과 전송  
        return updated;  
    }  
  
    public Article delete(Long id) { // 글 삭제  
  
        // 대상 조회  
        Article target = articleRepository.findById(id).orElse(null);  
  
        // 대상이 없으면 잘못된 요청으로 처리 - 400        
        if (target == null) {  
            return null;  
        }  
  
        // 대상이 있으면 삭제 진행  
        articleRepository.delete(target);  
        return target; // HTTP 응답의 body가 없는 ResponseEntity 생성  
    }  
}

jpa_service 2.png

jpa_service 3.png

jpa_service 4.png

jpa_service 5.png


Transaction

더 이상 나눌 수 없는 업무 처리의 최소 단위로, 모두 성공해야 하는 일련의 과정


REST API에 Transaction 추가하기

  1. ArticleApiController@PostMapping()으로 /api/transaction-test를 추가한다.
    • 요청에서 배열로 여러 개의 데이터를 받으므로 List<Article>로 데이터를 받아 Service로 전달한다.
package com.example.demo.api;  
  
import com.example.demo.DTO.ArticleForm;  
import com.example.demo.entity.Article;  
import com.example.demo.service.ArticleService;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.http.HttpStatus;  
import org.springframework.http.ResponseEntity;  
import org.springframework.web.bind.annotation.*;  
  
import java.util.ArrayList;  
import java.util.List;  
  
@RestController // REST를 적용한 Controller 사용 표시  
@RequestMapping("/api")  
@Slf4j // simple logging facade for java  
public class ArticleApiController {  
  
    @Autowired  
    private ArticleService articleService;  
  
    // ... 중략
  
    // POST - Transaction  
    @PostMapping("test") // 새 글 작성 - 3개 연속 생성  
    public ResponseEntity<List<Article>> transactionTest(@RequestBody List<ArticleForm> dtos) {  
        List<Article> createdList = articleService.createArticles(dtos);  
        return (createdList != null) ?  
                ResponseEntity.status(HttpStatus.OK).body(createdList) :  
                ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);  
    }  
  
    // ... 중략
}
  1. ArticleService에서도 List<Article>을 생성하는 메소드를 추가하고, 강제 예외를 발생시켜 Transaction을 테스트한다.
    • for문을 사용하여 List 내의 내용물들에 대해 Entity 변환과 DB 저장을 수행해도 되지만 Stream 문법을 사용하면 줄을 간략하게 정리해서 사용할 수 있다.
    • orElseThrow() : 값이 존재하면 그 값을 반환하고, 없으면 전달 받은 값으로 보낸 예외를 발생시킨다.
    • IllegalArgumentException : 전달값이 없거나 유효하지 않은 경우의 예외를 처리한다.
package com.example.demo.service;  
  
import com.example.demo.DTO.ArticleForm;  
import com.example.demo.entity.Article;  
import com.example.demo.repository.ArticleRepository;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
import org.springframework.web.bind.annotation.*;  
  
import java.util.ArrayList;  
import java.util.List;  
import java.util.stream.Collectors;  
  
@Service  
public class ArticleService {  
    @Autowired  
    private ArticleRepository articleRepository;  
	
	// ...중략 
  
    public List<Article> createArticles(List<ArticleForm> dtos) { // 새 글 작성 - 3개 연속 생성  
        // stream을 사용해 List 내의 AricleForm을 모두 Article로 변환  
        List<Article> articleList = dtos.stream() // stream변환  
                .map(dto -> dto.toEntity()) // map 함수로 각 요소에 대해 함수 실행  
                .collect(Collectors.toList()); // Stream -> List로 변환  
  
        // Article 데이터들을 DB에 저장  
        articleList.stream() // stream 변환  
                .forEach(article -> articleRepository.save(article)); 
                // forEach 함수로 각 요소에 대해 함수 실행  
  
        // transaction 테스트를 위해 강제 예외 발생  
        articleRepository.findById(-1L)  
                .orElseThrow(() -> new IllegalArgumentException("데이터 없음"));  
  
        return articleList;  
    }  
  
	// ...중략 
}

jpa_transaction 1.png

jpa_transaction 2.png

  1. 이제 Transaction을 메소드에 적용하기 위해 ArticleService 클래스의 createArticles() 메소드에 @Transactional Annotation을 추가해 해당 메소드를 하나의 Transaction으로 묶는다.
package com.example.demo.service;  
  
import com.example.demo.DTO.ArticleForm;  
import com.example.demo.entity.Article;  
import com.example.demo.repository.ArticleRepository;  
import jakarta.transaction.Transactional;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
import org.springframework.web.bind.annotation.*;  
  
import java.util.ArrayList;  
import java.util.List;  
import java.util.stream.Collectors;  
  
@Service  
public class ArticleService {  
    @Autowired  
    private ArticleRepository articleRepository;  
	
	// ...중략 
  
    @Transactional  
    public List<Article> createArticles(List<ArticleForm> dtos) { // 새 글 작성 - 3개 연속 생성  
        // stream을 사용해 List 내의 AricleForm을 모두 Article로 변환  
        List<Article> articleList = dtos.stream() // stream변환  
                .map(dto -> dto.toEntity()) // map 함수로 각 요소에 대해 함수 실행  
                .collect(Collectors.toList()); // Stream -> List로 변환  
  
        // Article 데이터들을 DB에 저장  
        articleList.stream() // stream 변환  
                .forEach(article -> articleRepository.save(article)); 
                // forEach 함수로 각 요소에 대해 함수 실행  
  
        // transaction 테스트를 위해 강제 예외 발생  
        articleRepository.findById(-1L)  
                .orElseThrow(() -> new IllegalArgumentException("데이터 없음"));  
  
        return articleList;  
    }  
  
	// ...중략 
}

jpa_transaction 3.png